package gmu.drr.modelInst.simple;

import java.util.ArrayList;

import cern.jet.random.*;
import cern.jet.random.engine.*;

import gmu.drr.entity.*;
import gmu.drr.entity.netFactory.ModelFactory;
import gmu.drr.runSynthSimple.SynthSimpleParameters;

/**
 * @author rothmd01
 *
 */
public class ModelFactorySimple extends ModelFactory {
	private static ModelFactorySimple inst = null;


	public static ModelFactorySimple getInstance() {
		if( inst == null ) {
			inst = new ModelFactorySimple();
		} 
		return inst;
	}

	public SynthSimpleParameters params;
	
	/**
	 * @return the params
	 */
	public SynthSimpleParameters getParams() {
		return params;
	}
	
	/**
	 * @param params the params to set
	 */
	public void setParams(SynthSimpleParameters params) {
		this.params = params;
	}
	
	protected ArrayList<Doc> buildDocs( final int numDocs ) {
		ArrayList<Doc> retVal;
		retVal = super.buildDocs( numDocs );
		// TODO build some basic data into the docs
		return retVal;
	}

	protected ArrayList<User> buildUsers( final int numUsers ) {
		ArrayList<User> retVal;
		// NOTE netNode built on in the User constructor
		retVal = super.buildUsers(numUsers);
		// TODO build some basic data into the users
		for( User u : retVal ) {
			AbstractContinousDistribution lambdaDist, rhoDist, tauDist;
			double lambda, rho, tau;
			NetNodeActSimple nodeAct;
			NetNode node = u.getNetNode();
			nodeAct = new NetNodeActSimple();
			nodeAct.setNetNode(node);
			
			/* ensure that excDist and supDist use the random seed, and are participating in the randomEngine */
			/*  OK, the excDist and supDist are simulation-time distributions to be set when the simulationRun is set up, NOT now 
//			excDist = new Beta( params.netNodeActExcParA, params.netNodeActExcParB, null);
			excDist = new Uniform(null);
			nodeAct.setExcDist(excDist);
//			supDist = new Beta( params.netNodeActSupParA, params.netNodeActSupParB, null);
			supDist = new Uniform(null);
			nodeAct.setSupDist(supDist);
			*/
			
			// now set the lambda, rho, and tau, based on the parameters, using a beta distribution
			lambdaDist = new Beta( params.netNodeActExcParA, params.netNodeActExcParB, params.factoryRandomEngine );
			lambda = lambdaDist.nextDouble();
			rhoDist = new Beta( params.netNodeActSupParA, params.netNodeActSupParB, params.factoryRandomEngine );
			rho = rhoDist.nextDouble();
			tauDist = new Beta( params.netNodeActThreshParA, params.netNodeActThreshParB, params.factoryRandomEngine );
			tau = tauDist.nextDouble();
			
			nodeAct.setLambda(lambda);
			nodeAct.setRho(rho);
			nodeAct.setThreshold(tau);
			
			node.setNetNodeActModel( nodeAct );
		}
		return retVal;
	}

	
	public ArrayList<NetEdge> buildEdges() {
		ModelSimple locModel;
		double degreeAvg = 0.0;
		
		// maintain a queue of users
		class queueEntry {
			int tDegree, cDegree;
			User user;
		}
		
		locModel = (ModelSimple) model;

		ArrayList<User> u = locModel.getUserList();
		ArrayList<NetEdge> retVal = new ArrayList<NetEdge>();
		ArrayList<queueEntry> q = new ArrayList<queueEntry>( u.size() );
		
		// embodies the assumption of userDegreeDist distribution of distribution of degrees by node
		for( User user : u ) {
			double initialEst;
			queueEntry qElem;
			qElem = new queueEntry();
			// for all users, determine the users target degree
			qElem.user = user;
			initialEst = locModel.getUserDegreeDist().nextDouble();
			qElem.tDegree = (int) Math.round(initialEst);
			// no - really.  i want everyone to have one connection.  otherwise, the world is lonely.
			qElem.tDegree = Math.max(qElem.tDegree, 1); 
			qElem.cDegree = 0;
			degreeAvg += qElem.tDegree;
			q.add(qElem);
		}
		degreeAvg /= (double) q.size();
		
		// just in case I decide to change my mind about allowing 0-degree elements
		for( queueEntry qelem : q ) {
			if ( qelem.tDegree < 1 ) q.remove( qelem );
		}
		
		while (q.size() > 1 ) {
			long randSrc, randTarg;
			long remTarg = Integer.MAX_VALUE;
			queueEntry qSrc, qTarg;
			NetEdge edge;
			// assumes uniform distribution across source/target nodes.  no self-assortion.
			// TODO alternate - attempt uniform across outstanding degree - graph probably looks signficantly different - check literature to see which is more 'realistic'
			// TODO also... leaves one poor schmoe of a node (normally high-degree) underserved - all remaining edges would need to be self-edges
			remTarg = q.size();
			randSrc = locModel.getRandNum().nextLong() % remTarg;
			randSrc = randSrc < 0 ? -randSrc : randSrc;
			randTarg = randSrc;
			// NOTE avoids self-edges
			while( randSrc == randTarg ) {
				randTarg = locModel.getRandNum().nextLong() % remTarg;
				randTarg = randTarg < 0 ? -randTarg : randTarg;
				if( remTarg < 2 ) 
					break;
			}
			qSrc = q.get( (int) randSrc);
			qTarg = q.get( (int) randTarg );
			
			// statement of invariance...
			assert qSrc.tDegree > 0;
			assert qTarg.tDegree > 0;
			edge = new NetEdge();
			edge.setHeadNode(qSrc.user.getNetNode());
			edge.setTailNode(qTarg.user.getNetNode());
			retVal.add( edge );
			qSrc.cDegree += 1;
			if( qSrc.cDegree >= qSrc.tDegree ) {
				q.remove( qSrc );
			}
			qTarg.cDegree += 1;
			if( qTarg.cDegree >= qTarg.tDegree ) {
				q.remove( qTarg );
			}
		}

		return retVal;
	}
	
	/**
	 * adds a NetEdgeWeightSimple to each NetEdge in the list
	 * The NetEdgeWeightSimple requires a weight parameter, to be applied as a scaling factor in later activation calculations. That weight parameter is taken here from a beta distribution, with parameters from the base parameter file
	 * Note that the weight distributions are not normalized in any way.
	 * @param edgeList
	 * @see NetEdgeWeightSimple
	 * @see Beta
	 */
	private void buildEdgeWeights( ArrayList<NetEdge> edgeList ) {
		NetEdgeWeightSimple locEdgeWt;
		AbstractContinousDistribution wtDist;
		wtDist = new Beta(params.netEdgeWeightParA, params.netEdgeWeightParB, null);
		for( NetEdge edge : edgeList ) {
			double wtDraw;
			wtDraw = wtDist.nextDouble();
			locEdgeWt = new NetEdgeWeightSimple();
			locEdgeWt.setWeight(wtDraw);
			edge.setNetEdgeWeightModel(locEdgeWt);
		}
	}

	private void buildRelationsModel() {
		// TODO Auto-generated method stub

	}

	private void buildUserPrefs() {
		// TODO Auto-generated method stub

	}

	private void buildDocGroupList(ArrayList<DocGroup> docGroupList) {
		// TODO Auto-generated method stub

	}

	private void buildUserGroupList(ArrayList<UserGroup> userGroupList) {
		// TODO Auto-generated method stub

	}

	@Override
	public ModelSimple buildModel() {
		ModelSimple locModel = null;
		assert params != null;
//		assert (params.model).getClass() == locModel.getClass();
		// build model
		locModel = (ModelSimple) params.model;
		locModel.setFactory(this);
		// set this up for broad use across the factory object
		model = locModel;
		
		// assign the random factory
		if( params.factoryRandomSeed > 0 )
			locModel.setRandSeed(params.factoryRandomSeed);
		// note - counting on the parameter parsing to have taken care of setting the seed in the random engine
		locModel.setRandNum( params.factoryRandomEngine );
		
		// assign the primary model random distributions and elements
		locModel.setKu( params.numberUserGroups );
		locModel.setKd( params.numberDocGroups );
		AbstractContinousDistribution tmpDist;
		tmpDist = new Exponential( params.userDegreeWeight, model.getRandNum() );
		locModel.setUserDegreeDist( tmpDist );
		
		// allocate users
		ArrayList<User> userList;
		userList = buildUsers( params.numberUsers );
		locModel.setUserList(userList);
		
		// allocate docs
		ArrayList<Doc> docList;
		docList = buildDocs( params.numberDocs );
		locModel.setDocList(docList);
		
		// build edges 
		ArrayList<NetEdge> edgeList;
		edgeList = buildEdges();
		locModel.setEdgeList(edgeList);
		// build weights for edges
		buildEdgeWeights( edgeList );
		
		// build user groups
		ArrayList<UserGroup> userGroupList;
		userGroupList = new ArrayList<UserGroup>( locModel.getKu() );
		buildUserGroupList( userGroupList );

		// build doc groups
		ArrayList<DocGroup> docGroupList;
		docGroupList = new ArrayList<DocGroup>( locModel.getKd() );
		buildDocGroupList( docGroupList );
		
		// build user preferences
		buildUserPrefs();
		
		// build relations model
		buildRelationsModel();
		
		return locModel;
	}

	protected ModelFactorySimple() {
		super();
	}
}
